{%-
set namespaces = package_name.split('.')
-%}
#pragma once
#include <tuple>
 
#include "eventuals/grpc/client.h" 
#include "eventuals/grpc/completion-thread-pool.h"
#include "eventuals/generator.h"
#include "eventuals/grpc/server.h"
#include "eventuals/task.h"
#include "eventuals/then.h"
#include "eventuals/expected.h"
#include "eventuals/let.h"
#include "eventuals/errors.h"

#include "grpcpp/completion_queue.h"

#include "{{ grpc_pb_header }}"

namespace {{ namespaces | join('::') }}::eventuals {

{% for service in services %}
struct {{ service.name }} {
  static constexpr char const* service_full_name() {
    return {{ namespaces | join('::') }}::{{ service.name }}::service_full_name();
  }

  class TypeErasedService : public ::eventuals::grpc::Service {
   public:
    ::eventuals::Task::Of<void> Serve() override;

    char const* name() override {
      return {{ service.name }}::service_full_name();
    }

   protected:
    virtual ~TypeErasedService() = default;
{% for method in service.methods %}
{%- set output_type -%}
    ::eventuals::{#...#}
    {%- if method.server_streaming -%}
        Generator::Of
    {%- else -%}
        Task::Of
    {%- endif -%}
    {#...#}<const {{ method.output_type.split('.') | join('::') }}&>::Raises<::eventuals::TypeErasedError>
{%- endset %}
{%- set input_type -%}
{# Input type does not include pointer #}
    {%- if method.client_streaming -%}
        ::eventuals::grpc::ServerReader<{{ method.input_type.split('.') | join('::') }}>
    {%- else -%}
        {{ method.input_type.split('.') | join('::') }}
    {%- endif -%}
{%- endset %}
    virtual {{ output_type }} TypeErased{{ method.name }}(
        std::tuple<
            TypeErasedService*, // this
            ::grpc::GenericServerContext*,
            {{ input_type }}*>* args) = 0;
{% endfor %}

    private:
{% for method in service.methods %}
    [[nodiscard]] ::eventuals::Task::Of<void> Serve{{ method.name }}();
{% endfor %}    
  };

  template <typename Implementation>
  class Service : public TypeErasedService {

{%- for method in service.methods -%}
{%- set output_type -%}
    ::eventuals::{#...#}
    {%- if method.server_streaming -%}
        Generator::Of
    {%- else -%}
        Task::Of
    {%- endif -%}
    {#...#}<const {{ method.output_type.split('.') | join('::') }}&>::Raises<::eventuals::TypeErasedError>
{%- endset %}
{%- set input_type -%}
{# Input type does not include pointer #}
    {%- if method.client_streaming -%}
        ::eventuals::grpc::ServerReader<{{ method.input_type.split('.') | join('::') }}>
    {%- else -%}
        {{ method.input_type.split('.') | join('::') }}
    {%- endif -%}
{%- endset %}
    {{ output_type }} TypeErased{{ method.name }}(
        std::tuple<
            TypeErasedService*,
            ::grpc::GenericServerContext*,
            {{ input_type }}*>* args) override {
      return [args]() {
{%- if not method.server_streaming and not method.client_streaming %}
{#- No streaming => then and move #}
        return ::eventuals::Then([args]() mutable {
          return std::apply(
              [](auto* implementation, auto* context, auto* request) {
                static_assert(std::is_base_of_v<Service, Implementation>);
                return dynamic_cast<Implementation*>(implementation)
                    ->{{ method.name }}(context, std::move(*request));
              },
              *args);
        });
{%- elif not method.server_streaming and method.client_streaming %}
{#- Client streaming => then #}
        return ::eventuals::Then([args]() mutable {
          return std::apply(
              [](auto* implementation, auto* context, auto* reader) {
                static_assert(std::is_base_of_v<Service, Implementation>);
                return dynamic_cast<Implementation*>(implementation)
                    ->{{ method.name }}(context, *reader);
              },
              *args);
        });
{%- elif method.server_streaming and not method.client_streaming %}
{#- Server streaming => move #}
        return std::apply(
            [](auto* implementation, auto* context, auto* request) {
              static_assert(std::is_base_of_v<Service, Implementation>);
              return dynamic_cast<Implementation*>(implementation)
                  ->{{ method.name }}(context, std::move(*request));
            },
            *args);
{%- elif method.server_streaming and method.client_streaming %}
{#- Bi-directional streaming => neither #}
        return std::apply(
            [](auto* implementation, auto* context, auto* reader) {
              static_assert(std::is_base_of_v<Service, Implementation>);
              return dynamic_cast<Implementation*>(implementation)
                  ->{{ method.name }}(context, *reader);
            },
            *args);
{%- endif %}
      };
    }
{%- endfor %}
  };

  class Client {
   public:
    explicit Client(
      const std::string& target,
      const std::shared_ptr<::grpc::ChannelCredentials>& credentials,
      stout::borrowed_ref<::eventuals::grpc::ClientCompletionThreadPool>&& pool)
        : client_(target, credentials, std::move(pool)) {}

    explicit Client(
      std::shared_ptr<::grpc::Channel> channel,
      stout::borrowed_ref<::eventuals::grpc::CompletionThreadPool<::grpc::CompletionQueue>>&& pool)
        : client_(channel, std::move(pool)) {}

{% for method in service.methods %}
{%- set input_type -%}
    {%- if method.client_streaming -%}
        ::eventuals::grpc::Stream<{{ method.input_type.split('.') | join('::') }}>
    {%- else -%}
        {{ method.input_type.split('.') | join('::') }}
    {%- endif -%}
{%- endset %}
{%- set output_type -%}
    {%- if method.server_streaming -%}
        ::eventuals::grpc::Stream<{{ method.output_type.split('.') | join('::') }}>
    {%- else -%}
        {{ method.output_type.split('.') | join('::') }}
    {%- endif -%}
{%- endset %}
{%- if not method.server_streaming and not method.client_streaming %}
    [[nodiscard]] auto {{ method.name }}({{ method.input_type.split('.') | join('::') }}&& request) {
        return client_.Call<
            {{ namespaces | join('::') }}::{{ service.name }},
            {{ input_type }},
            {{ output_type }}>("{{ method.name }}")
        >> ::eventuals::Then(::eventuals::Let([request = std::move(request)](auto& call) {
            return call.Writer().WriteLast(request)
                >> call.Reader().Read()
                >> ::eventuals::Head()
                >> Finally(::eventuals::Let([&](auto& response) {
                    return call.Finish()
                        // TODO: Update the error in `expected` to be eventuals::RuntimeError.
                        >> ::eventuals::Then([&](grpc::Status&& status) -> ::eventuals::expected<{{ output_type }}, std::variant<::eventuals::Stopped, ::eventuals::RuntimeError>>{
                            if (status.ok()) {
                                return std::move(response);
                            } else {
                                return ::eventuals::make_unexpected(
                                        ::eventuals::RuntimeError(
                                            "Failed to '{{ service.name }}.{{ method.name}}', "
                                            "received gRPC status: "
                                            + std::to_string(status.error_code())
                                            + " and error message: "
                                            + std::string(status.error_message())));
                            }
                        });
                    }));
            }));
    }
{%- else %}
    [[nodiscard]] auto {{ method.name }}() {
        return client_.Call<
            {{ namespaces | join('::') }}::{{ service.name }},
            {{ input_type }},
            {{ output_type }}>("{{ method.name }}");
    }
{% endif %}
{% endfor %}

     protected:
      ::eventuals::grpc::Client client_;
    };
};
{%- endfor %}

} // namespace {{ namespaces | join('::') }}::eventuals

